【go-zero】API DSL

  • ~6.01K 字
  1. 1. API DSL
    1. 1.1. 文件结构
    2. 1.2. 服务块
  2. 2. API 类型
    1. 2.1. 参数
      1. 2.1.1. 参数接收规则
      2. 2.1.2. 参数校验规则
  3. 3. 路由规则
  4. 4. 路由分组
  5. 5. 路由前缀
  6. 6. 中间件声明
  7. 7. JWT鉴权
  8. 8. 请求签名
  9. 9. API导入
  10. 10. SSE路由
    1. 10.1. 工作原理
    2. 10.2. SSE支持

API DSL

goctl API DSL是一种描述HTTP服务的简洁语言,来源于.api文件

文件结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
syntax = "v1"         // 必须的版本声明

info ( // 可选元数据块
title: "用户 API"
version: "1.0"
)

import "shared.api" // 导入其他 .api 文件

type (...) // 类型定义

service name-api { // 服务块
@server (...)
@handler HandlerName
method /path (RequestType) returns (ResponseType)
}

服务块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
service user-api {
@server (
jwt: Auth // 启用 JWT 中间件
middleware: AccessLog,Cors // 应用命名中间件
prefix: /v1 // URL 前缀
timeout: 3s // 超时时间
)

@handler Login
post /user/login (LoginReq) returns (LoginResp)

@handler GetUser
get /user/:id (UserReq) returns (UserResp)
}

API 类型

类型声明要满足如下规则:

  • 以type开头
  • 不需要声明struct关键字
  • 不支持泛型和弱类型

参数

参数接收规则

可以在类型中用tag声明参数接收规则

接收规则 说明 生效范围 接收tag示例 请求示例
json json 序列化 请求体&响应体 json:“foo” {“key”:“value”}
path 路由参数 请求体 path:“id” /foo/:id
form post 请求的表单参数请求接收标识,get 请求的 query 参数接收标识 请求体 form:“name” GET /search?key=value
header http 请求体接收标识 请求体 header:“Content-Length” origin: https://go-zero.dev

其中POST支持 content-type 为 form-data 和 x-www-form-urlencoded

注意:gozero中不支持多tag接收参数,即一个字段只能有一个tag

参数校验规则

还可以在tag中添加参数校验,只对请求体有效,支持的规则如下:

接收规则 说明 示例
optional 当前字段是可选参数,允许为零值(zero value) json:“foo,optional”
options 当前参数仅可接收的枚举值 json:“gender,options=foo|bar”
default 当前参数默认值 json:“gender,default=male”
range 当前参数数值有效范围,仅对数值有效 json:“age,range=[0:120]”

注意,range表达式用小括号表示开区间,中括号表示闭区间,当min值缺省时,min代表0,max值缺省时,mnax代表无穷,两者不能同时缺省

路由规则

API描述语言中,路由需要满足如下规则:

  • 以/开头
  • 路由节点以/分隔
  • 路由节点中可以包含:,但:必须是路由节点的第一个字符,它后面的节点值必须在请求体中有pathtag声明,用于接收路由参数
  • 路由节点可以包含数字、字母、下划线和中划线
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
service Demo {
// 示例路由 /foo
@handler demoPath1
get /foo (DemoReq) returns (DemoResp)

// 示例路由 /foo/bar
@handler demoPath2
get /foo/bar (DemoReq) returns (DemoResp)

// 示例路由 /foo/bar/:id,其中 id 为请求体中的字段
@handler demoPath3
get /foo/bar/:id (DemoPath3Req) returns (DemoResp)

// 示例路由 /foo/bar/:id/:name,其中 id,name 为请求体中的字段
@handler demoPath4
get /foo/bar/:id/:name (DemoPath4Req) returns (DemoResp)

// 示例路由 /foo/bar/baz-qux
@handler demoPath6
get /foo/bar/baz-qux (DemoReq) returns (DemoResp)

// 示例路由 /foo/bar_baz/123(goctl 1.5.1 支持)
@handler demoPath7
get /foo/bar_baz/123 (DemoReq) returns (DemoResp)
}

路由分组

随着业务扩展,服务接口也会变多,生成代码文件也会变多,我们需要将生成的代码文件按照维度聚合,便于开发和维护

没有分组的情况,生成的代码handler和logic下的目录都是混在一起的,不方便管理和阅读

API语言中,我们可以通过在@server语句块中使用group关键字分组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@server (
prefix: /v1
group: user
)
service user-api {
//...
}

@server (
prefix: /v1
group: role
)
service user-api {
//...
}

@server (
prefix: /v1
group: class
)
service user-api {
//...
}

这样可以将不同的业务逻辑分到不同的目录下

路由前缀

假设我们有一个用户服务,需要通过路由来区分不同版本,可以通过api语言声明路由前缀

1
2
https://example.com/v1/users
https://example.com/v2/users

可以通过在@server语句块中使用prefix关键字声明路由前缀:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@server (
prefix: /v1
)
service user-api {
@handler usersv1
get /users returns ([]UserV1)
}

@server (
prefix: /v2
)
service user-api {
@handler usersv2
get /users returns ([]UserV2)
}

生成的代码会通过rest.WithPrfix来声明路由前缀

中间件声明

API语言中,在@server语句块中使用middleware关键字声明中间件的,多个中间件以英文逗号分割,这样生成的代码会单独开一个middleware文件夹

1
2
3
4
@server(
// 通过 middileware 关键字声明中间件,多个中间件以英文逗号分割,如 UserAgentMiddleware,LogMiddleware
middleware: UserAgentMiddleware
)

中间件代码由goctl自动生成,是一个结构体,存在一个Handle方法,接收一个http.HandlerFunc参数并返回一个http.HandlerFunc参数,用于对请求处理并传递给下一个中间件或handler

例:我们在中间件中将header的User-Agent信息存到context中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type UserAgentMiddleware struct {
}

func NewUserAgentMiddleware() *UserAgentMiddleware {
return &UserAgentMiddleware{}
}

func (m *UserAgentMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
val := r.Header.Get("User-Agent")
reqCtx := r.Context()
ctx := context.WithValue(reqCtx, "User-Agent", val)
newReq := r.WithContext(ctx)
next(w, newReq)
}
}

context的传递:

  • 在一个新中间件中间件中,使用r.Context()获取传入请求的context
  • 使用context.WithValue()新建带值的ctx
  • 使用r.WithContext()得到新的request,传到下游
  • 下游代码可以通过r.Context()获取值

JWT鉴权

JWT是一种开放标准,用于在网络应用间传递声明式信息,是一种基于JSON的轻量级身份验证和授权机制,用于在客户端和服务端之间安全传输信息

在API文件中,在@server语句块中使用jwt:Auth开启jwt认证,它仅对下面对应的路由有效,使用Auth作为jwt的值,经过goctl代码生成后会转成对应的jwt配置:

1
2
3
4
5
6
7
8
9
10
11
12
package config

import "github.com/zeromicro/go-zero/rest"

type Config struct {
rest.RestConf
// Auth 配置 JWT 认证需要的密钥和过期时间。
Auth struct {
AccessSecret string
AccessExpire int64
}
}

route.go中生成了通过rest.WithJwt来声明jwt认证

注意:对于jwt token的生成和refresh token仍需开发者自行实现

jwt通常也可以携带一些自定义信息,比如server端生成jwt key时添加了custom-key值,go-zero解析后会将所有载体放到context中,获取载体信息示例如下:

1
2
3
4
5
func (l *UserInfoLogic) UserInfo(req *types.UserInfoReq) (resp *types.UserInfoResp, err error) {
// 获取 jwt 载体信息
value := l.ctx.Value("custom-key")
return
}

请求签名

gozero中内置了签名功能,可以通过api语言开启,并生成签名代码

在@server代码块中使用signature关键字开启签名功能

1
2
3
4
5
6
7
@server (
signature: true // 通过 signature 关键字开启签名功能
)
service sign-api {
@handler SignDemo
post /sign/demo (SignDemoReq) returns (SignDemoResp)
}

生成的路由代码会自动携带rest.WithSignature

API导入

随着业务量增大,API文件也会越来越大,写在同一个api文件中api文件也会变得巨大,不易阅读和维护,所以可以通过api import引入其他api文件

假设HTTP服务响应统一为如下json格式:

1
2
3
4
5
{
"code": 0,
"msg": "success",
"data": {}
}

前两个是固定的,data可变,所以我们可以将前两个字段抽象出来,定义一个公共结构体,在其他api文件中引入该结构体,例如抽象在bash.api中:

1
2
3
4
5
6
syntax =  "v1"

type Base {
Code int `json:"code"`
Msg string `json:"msg"`
}

这样其他api文件可以通过import "base.api"引入

由于api描述语言中没有package的概念,所以引入时需要用相对路径

SSE路由

SSE是服务器推送事件,一种服务器向浏览器单向推送实时数据的轻量协议

工作原理

  • 客户端发起一个普通HTTP请求,且header中告诉服务器自己支持SSE
  • 服务器保持该连接不关闭,并持续推送文本消息(以text/event-stream 格式)
  • 每条消息都是一个事件,浏览器通过js的EventSource对象自动接收,不用反复轮询

SSE支持

gozero中可以通过在@server语句块中开启sse:true注解来支持SSE

指定开启后,goctl会带有rest.WithSSE()选项的路由,该选项会自动:

  • 设置适当的SSE头部
  • 移除写入超时以允许长时间运行的连接
  • 启用连接保持活动

注意:

  • SSE路由通常应该使用GET方法
  • 连接将保持打开,知道客户端断开连接或处理程序完成
  • SSE可与JWT认证和中间件良好配合使用
赞助喵
非常感谢您的喜欢!
赞助喵